superstrict

import BRL.LinkedList
import BRL.Stream
import BRL.FileSystem

import "../../framework/keycodes/keycodes.bmx"
import "../strutil/strutil.bmx"
import "SourceLine.bmx"
import "Error.bmx"

type SourceFile

	'// -------------------------------------------------------------------------------------------------------------
	'// Returns a new 'SourceFile' object.
	'// Read a source file to memory, and clean it up, by removing blank files, tabs, spaces and comments.
	'// -------------------------------------------------------------------------------------------------------------
	function create:SourceFile(url:string, errors:TList)
	
		if (fileType(url) <> FILETYPE_FILE) then
		
			writestderr(stripAll(appFile) + ": Could not locate file ~q" + url + "~q.~n");
			return null;
		
		end if
		
		local fileStream:TStream = readStream(url);
		
		if (not fileStream) then
		
			writestderr(stripAll(appFile) + ": Could not read file ~q" + url + "~q.~n");
			return null;
		
		end if
		
		local src:SourceFile = new SourceFile;
		src.m_path = url;
		
		local insideString:byte = false;
		local insideChar:byte = false;
		local insideSLComment:byte = false;
		local insideMLComment:byte = false;
		
		local addChar:byte = false;
		
		local peek:byte = 0;
		local lineData:string = null;
		local lineNumber:int = 1;
		
		'// Read the source file to memory, removing whitespaces (tabs, spaces, carriage return and blank lines),
		'// and remove comments.
		while (not fileStream.eof())
		
			addChar = false;
			
			'// Try to read a byte from the source file.
			try
			
				peek = fileStream.readByte();
			
			catch e:TStreamReadException
			
				writestderr(stripAll(appFile) + ": Unable to read data from ~q" + url + "~q.~n");
				return null;
			
			end try
			
			'// Skip carriage returns. We don't support Mac OS-9 or earlier. Everything else is supported.
			if (peek = $0d) ..
				continue;
			
			'// Add a line of code to the memory, if we found a line feed. Also, increase the line number by 1.
			if (peek = $0a) then
			
				src.addLineLast(lineData, lineNumber);
				
				lineData = null;
				lineNumber :+ 1;
			
			end if
			
			'// If we didn't find a line feed, determine whether or not to add a character.
			if (not addChar) then
			
				'// If we're inside a string quotation.
				if (insideString) then
				
					if (peek = KEY_QUOTES) ..
						insideString = false;
					
					addChar = true;
				
				'// If we're inside a character quotation.
				else if (insideChar) then
				
					if (peek = KEY_APOSTR) ..
						insideChar = false;
					
					addChar = true;
				
				'// If we're inside a single-line comment.
				else if (insideSLComment) then
				
					if (peek = $0a) ..
						insideSLComment = false;
				
				'// If we're inside a multi-line comment.
				else if (insideMLComment) then
				
					'// We found a */ we are no longer in a multi-line comment.
					if (not fileStream.eof() and peek = KEY_ASTERISK) then
					
						if (fileStream.readByte() = KEY_SLASH) ..
							insideMLComment = false; ..
						else ..
							fileStream.seek(fileStream.pos()-1);
					
					end if
				
				'// If we're reading regular code.
				else
				
					'// If we found a // or a /*, we're now in a comment.
					if (not fileStream.eof() and peek = KEY_SLASH and not insideString and not insideChar) then
					
						if (fileStream.readByte() = KEY_ASTERISK) ..
							insideMLComment = true; ..
						else ..
							fileStream.seek(fileStream.pos()-1);
						
						if (fileStream.readByte() = KEY_SLASH) ..
							insideSLComment = true; ..
						else ..
							fileStream.seek(fileStream.pos()-1);
					
					'// If we found a ", we're now in a string quotation.
					else if (peek = KEY_QUOTES) then
					
						insideString = true;
					
					'// If we found a ', we're now in a character quotation.
					else if (peek = KEY_APOSTR) then
					
						insideChar = true;
					
					end if
					
					'// Add the character, unless we're in a comment.
					if (not insideSLComment and not insideMLComment) ..
						addChar = true;
				
				end if
			
			end if
			
			'// Add a character.
			if (addChar) then
			
				if (peek = $0a) then
				
					src.addLineLast(lineData, lineNumber);
					lineData = null;
				
				else
				
					lineData :+ chr(peek);
				
				end if
			
			end if
		
		end while
		
		'// Catch unterminated multi-line comments.
		if (insideMLComment) then
		
			errors.addLast(Error.createError( ..
				Error.UNTERMINATED_COMMENT, ..
				SourceLine(src.m_lines.last()), ..
				null));
		
		end if
		
		fileStream.close();
		
		return src;
	
	end function
	
	'// -------------------------------------------------------------------------------------------------------------
	'// Add a new line of data last in the line list.
	'// -------------------------------------------------------------------------------------------------------------
	method addLineLast(data:string, lineNumber:int)
	
		data = strutil.stripExtraWhitespaces(data);
		
		if (not data) ..
			return;
		
		local line:SourceLine = new SourceLine;
		
		m_lines.addLast(line);
		
		line.m_link = m_lines.lastLink();
		line.m_data = data;
		line.m_lineNumber = lineNumber;
		line.m_source = self;
		line.m_originPath = m_path;
	
	end method
	
	'// -------------------------------------------------------------------------------------------------------------
	'// Remove a line from the line list, and return the next line.
	'// -------------------------------------------------------------------------------------------------------------
	method remove:SourceLine(line:SourceLine)
	
		local oldLine:SourceLine = line;
		
		line = SourceLine(line.m_link._succ.value());
		if (line = m_lines.first()) ..
			line = null;
		
		oldLine.m_link.remove();
		
		return line;
	
	end method
	
	'// -------------------------------------------------------------------------------------------------------------
	'// The path to the file that this SourceFile object contains.
	'// -------------------------------------------------------------------------------------------------------------
	field m_path:string;
	
	'// -------------------------------------------------------------------------------------------------------------
	'// A linked list of all the lines of the source file.
	'// -------------------------------------------------------------------------------------------------------------
	field m_lines:TList = new TList;

end type
